package bma.common.langutil.log.filelog;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import bma.common.langutil.core.DateTimeUtil;
import bma.common.langutil.core.ValueUtil;
import bma.common.langutil.io.IOUtil;

public class AsyncFileLog implements LogWriter {

	final org.slf4j.Logger log = org.slf4j.LoggerFactory
			.getLogger(AsyncFileLog.class);

	private class MyThread extends Thread {
		public MyThread() {
			super("AsyncFileLogThread");
		}

		@Override
		public void run() {
			threadRun();
		}
	}

	// 配置属性
	private File logDirFile;
	private String groupId;
	private long flushMilliseconds = 1000;
	private SimpleDateFormat logNameFormat = new SimpleDateFormat("yyyyMMdd");
	private Charset encoding = Charset.defaultCharset();
	private int capacity = 10000;
	// end 配置属性

	private AtomicBoolean stoped = new AtomicBoolean();
	private MyThread thread;
	private BlockingQueue<FileLogTask> tasks;

	private static class LogFileItem {
		String logId;
		long rollTime;
		File file;
		Writer out;
	}

	private Map<String, LogFileItem> files = new HashMap<String, AsyncFileLog.LogFileItem>();

	public long getFlushMilliseconds() {
		return flushMilliseconds;
	}

	public void setFlushMilliseconds(long flushMilliseconds) {
		this.flushMilliseconds = flushMilliseconds;
	}

	public File getLogDirFile() {
		return logDirFile;
	}

	public void setLogDirFile(File logDirFile) {
		this.logDirFile = logDirFile;
	}

	public void setLogDir(String dir) {
		this.logDirFile = new File(dir);
	}

	public String getGroupId() {
		return groupId;
	}

	public void setGroupId(String groupId) {
		this.groupId = groupId;
	}

	public SimpleDateFormat getLogNameFormat() {
		return logNameFormat;
	}

	public void setLogNameFormat(SimpleDateFormat logNameFormat) {
		this.logNameFormat = logNameFormat;
	}

	public void setLogName(String v) {
		logNameFormat = new SimpleDateFormat(v);
	}

	public void setEncoding(String encoding) {
		this.encoding = Charset.forName(encoding);
	}

	public void start() {
		if(tasks == null){
			tasks = new LinkedBlockingQueue<FileLogTask>(getCapacity());
		}
		if (this.logDirFile == null) {
			throw new NullPointerException("logDirFile");
		}
		if (!this.logDirFile.exists()) {
			throw new IllegalArgumentException("'" + this.logDirFile
					+ "' not exists");
		}

		if (thread != null) {
			stop();
		}
		thread = new MyThread();
		thread.start();
	}

	public void stop() {
		if (thread != null) {
			if (thread.isAlive()) {
				stoped.set(true);
				thread.interrupt();
				try {
					thread.join(60 * 1000);
				} catch (InterruptedException e) {
				}
			}
			thread = null;
		}
	}

	@Override
	public boolean write(FileLogTask task) {
		if (stoped.get()) {
			if (log.isDebugEnabled()) {
				log.debug("stoped,skil {}", task);
			}
			return false;
		}
		tasks.offer(task);
		return true;
	}

	protected void threadRun() {
		try {
			if (log.isDebugEnabled()) {
				log.debug("thread start");
			}
			while (true) {
				FileLogTask task = tasks.poll(flushMilliseconds,
						TimeUnit.MILLISECONDS);
				if (task == null) {
					flushAllFile();
					continue;
				}
				handleTask(task);
				if (stoped.get()) {
					if (log.isDebugEnabled()) {
						log.debug("stoped");
					}
					break;
				}
			}
		} catch (InterruptedException e) {
			stoped.set(true);
			if (log.isDebugEnabled()) {
				log.debug("thread interrupted stop");
			}
		}catch(Throwable e){
			log.error("thread Throwable stop",e);
		} finally {
			flushAll();
			closeAllFile();
		}
	}

	protected void flushAll() {
		if (tasks.isEmpty())
			return;
		if (log.isDebugEnabled()) {
			log.debug("flush all tasks");
		}
		while (!tasks.isEmpty()) {
			FileLogTask task = tasks.poll();
			if (task == null)
				continue;
			handleTask(task);
		}
	}

	private class MyRealm implements LogWriterRealm {

		@Override
		public void write(String logId, String content) {
			realmWrite(logId, content);
		}
	}

	private final MyRealm realm = new MyRealm();

	protected void handleTask(FileLogTask task) {
		try {
			task.run(realm);
		} catch (Throwable e) {
			if (log.isWarnEnabled()) {
				log.warn("handle(" + task + ")", e);
			}
		}
	}

	protected void closeFile(LogFileItem item) {
		if (item.out != null) {
			if (log.isDebugEnabled()) {
				log.debug("close {}", item.file);
			}
			try {
				item.out.flush();
			} catch (Exception e) {
			}
			IOUtil.close(item.out);
		}
	}

	protected void closeAllFile() {
		for (LogFileItem item : files.values()) {
			closeFile(item);
		}
		files.clear();
	}

	protected void flushAllFile() {
		for (LogFileItem item : files.values()) {
			try {
				if (item.out != null) {
					item.out.flush();
				}
			} catch (Exception err) {
				if (log.isWarnEnabled()) {
					log.warn("flush " + item.file + " fail", err);
				}
			}
		}
	}

	protected void realmWrite(String logId, String content) {
		LogFileItem item = files.get(logId);
		if (item == null) {
			item = new LogFileItem();
			item.logId = logId;
			files.put(logId, item);
		}
		try {
			if (item.rollTime < System.currentTimeMillis()) {
				closeFile(item);
				item.out = null;
				// roll it
				Date now = DateTimeUtil.setTime(new Date(), 0, 0, 0);
				item.rollTime = now.getTime() + 24 * 3600 * 1000;
				item.file = new File(this.logDirFile, logId
						+ (ValueUtil.empty(groupId) ? "" : "-" + groupId) + "."
						+ this.logNameFormat.format(now) + ".log");
			}
			if (item.out == null) {
				if (log.isDebugEnabled()) {
					log.debug("create logfile {}", item.file);
				}
				item.out = new OutputStreamWriter(new FileOutputStream(
						item.file, true), this.encoding);
			}

			if (content != null) {
				if (log.isDebugEnabled()) {
					log.debug("writelog({},{})", item.logId, content.trim());
				}
				item.out.write(content);
			}
		} catch (Exception err) {
			log.error("write log (" + item.file + ") fail", err);
			return;
		}

	}

	public void setCapacity(int capacity) {
		this.capacity = capacity;
	}

	public int getCapacity() {
		return capacity;
	}
}
